feat(cli): port backups list and restore to native TypeScript#5331
Merged
Conversation
Replaces the Phase-0 Go-proxy handlers for `supabase backups list` and `supabase backups restore` with native Effect-based implementations. Adds the supporting legacy infrastructure (auth, config, project-ref resolution, Glamour table renderer) that future ports will reuse. - Strict Go parity on stdout/stderr: byte-identical `--output json` (alphabetical field order, `backups: null` for nil slices), Glamour tables, restore stderr line. - Extends `Output` with a `raw(text, stream)` method so handlers route stdout/stderr through the service. Removes monkey-patching from integration tests. - Hoists shared backups layer composition (`backups.layers.ts`) and HTTP error mapping (`mapLegacyBackupHttpError` factory) so subsequent subcommands stay DRY. - Truncates API error response bodies to 1024 chars in tagged-error fields. - Suppresses the fetching/restoring spinner in non-text output modes. - Regenerates `@supabase/api` contracts from upstream OpenAPI (brings in the missing `id` field on backup items).
…time The bundled `supabase-legacy` binary panicked at runtime with `Service not found: supabase/legacy/CliConfig` when running `backups list` / `backups restore` (CI e2e parity shard). `legacyProjectRefLayer` reads `LegacyCliConfig` directly for workdir and projectId resolution. `Layer.provide` satisfies a target layer's requirement but does not expose the provided service to siblings of a `Layer.mergeAll(...)`, so providing `legacyCliConfigLayer` only to `legacyBackupsPlatformApiLayer` is not enough. Restore the explicit `Layer.provide(legacyCliConfigLayer)` on the project-ref arm of `legacyBackupsRuntimeLayer` and document the reasoning in a header comment so the trap isn't reintroduced. Verified by building `dist/supabase-legacy` and invoking both commands; they now reach the API and surface the expected 401 instead of the service-resolution panic.
…ity) The cli-e2e harness writes a per-test YAML profile to a temp path and exports SUPABASE_PROFILE=<that-path> for both the Go and ts-legacy binaries. Go's LoadProfile (apps/cli-go/internal/utils/profile.go) implements dual semantics: built-in profile name first, YAML file path second. The native TS port only handled the built-in branch, so unknown tokens silently fell back to "supabase" — every API call hit api.supabase.com instead of the local replay server and parity tests failed with HTTP 401. Mirror Go's dual semantics in `legacyCliConfigLayer`: if SUPABASE_PROFILE isn't a built-in name, treat it as a YAML config-file path and read `api_url` / `name` from it. Fall back to the `supabase` built-in if the file is missing or malformed. Widen `LegacyCliConfig.profile` from the string-literal union to `string` since the YAML `name:` is arbitrary user input (the sole consumer reads it as a keyring account name). Adds three unit tests covering the new branch, documents the dual semantics in both backups SIDE_EFFECTS files, and updates the stale "ts-legacy shells out to Go" comment in the cli-test-helpers harness.
@clack/prompts' log.error / log.message default to process.stdout. The
Go CLI writes failure messages to stderr, so any native-ported legacy
command's error output landed on the wrong stream — visible in
cli-e2e parity tests as "stderr differs" for backups list/restore
401/403/404/429/500/422 error paths, and as testBehaviour assertions
on result.stderr.toContain(...) failing because stderr was empty.
Pass `{ output: process.stderr }` to clack's log.error / log.message
calls in textOutputLayer.fail so the error block ("■ <msg>") and gray
detail line render on stderr. The outro suggestion stays on stdout
(matches clack's intro/outro convention; not load-bearing for parity).
The Go CLI wraps http.DefaultTransport with a stderr logger when --debug is set (apps/cli-go/internal/debug/http.go), producing "HTTP <ts> <METHOD>: <URL>" lines. cli-e2e asserts on this for `backups list --debug` and the native legacy port had no equivalent. Add `legacyHttpClientLayer` (apps/cli/src/legacy/auth/legacy-http-debug.layer.ts) that conditionally wraps `FetchHttpClient.layer` with an `HttpClient.mapRequest` middleware reading `LegacyDebugFlag`. When the flag is unset the layer is identity over FetchHttpClient; when set, every outgoing request prints `HTTP YYYY/MM/DD HH:MM:SS <METHOD>: <URL>\n` to stderr in Go's exact `log.LstdFlags|log.Lmsgprefix` format. Wire into backups.layers.ts in place of `FetchHttpClient.layer`. Subsequent native ports composing through `legacyBackupsRuntimeLayer` or copying its pattern inherit the same behavior.
Replaces clack's `log.error` framing (`│` guide + `■` icon) with raw
process.stderr.write that mirrors Go's `recoverAndExit`
(apps/cli-go/cmd/root.go:300-303): a single red-styled message line
followed by an optional suggestion. When the caller doesn't provide
its own suggestion, fall back to Go's `SuggestDebugFlag` string
("Try rerunning the command with --debug to troubleshoot the error.")
— unless `--debug` is already set, matching the Go gate.
Also: the Go binary prints github.com/go-errors/errors stack frames
before the message in dev builds (`utils.Version == ""`). cli-e2e
exercises these dev builds, so even after byte-matching the message the
parity comparison would diff on the leading stack-trace block. The TS
port intentionally doesn't reconstruct these frames; add a normalize
rule in packages/cli-test-helpers/src/normalize.ts to strip the Go
frame block (matched after rules 8 and 10 normalize the path and
address to literals) plus the trailing blank line.
Updates the existing `output.layer.unit.test.ts` `fail` test to assert
on raw stderr bytes; adds two cases for the --debug suggestion
fallback (present when --debug unset, omitted when set).
Mirrors Go's ensureProjectGroupsCached (apps/cli-go/cmd/root.go:213-234): when --project-ref is set and supabase/.temp/linked-project.json doesn't exist yet, fetch GET /v1/projects/<ref> and write the project metadata to the cache. Adds: - LegacyLinkedProjectCache service + layer at apps/cli/src/legacy/telemetry/. Best-effort: auth, network, schema, and filesystem errors are all swallowed (matches Go's "log to debug and return" behavior). - Uses HttpClient directly rather than the typed LegacyPlatformApi client. The generated V1ProjectWithDatabaseResponse schema enforces a 20-char project-ref length that cli-e2e replay fixtures (which store `__PROJECT_REF__` placeholder strings) cannot satisfy. The cache only reads four string fields and doesn't validate them. - Body shape matches LinkedProject from apps/cli-go/internal/telemetry/project.go:15-20. - Hook from each backups handler via Effect.ensuring(cache.cache(ref)) so the cache fires whether the main API call succeeds or fails (matches Go's PersistentPostRun). Updates integration test setups to provide a no-op mockLinkedProjectCacheLayer so the new service requirement doesn't break the existing handler tests.
Mirrors Go's LoadOrCreateState (apps/cli-go/internal/telemetry/state.go:74-98): on every command invocation, write a JSON telemetry-state file with a persistent device_id, a session_id that rotates after 30 minutes of inactivity, the current timestamp, and a schema_version. The file lives at $SUPABASE_HOME/telemetry.json (or ~/.supabase/telemetry.json), matching Go's telemetryPath() exactly. Adds: - LegacyTelemetryState service + layer at apps/cli/src/legacy/telemetry/. Best-effort: filesystem and JSON-parse errors are swallowed. - Field order matches Go's struct declaration: enabled, device_id, session_id, session_last_active, distinct_id?, schema_version. The enabled flag stays true on fresh creation; only the user's `supabase telemetry disable` flips it. `SUPABASE_TELEMETRY_DISABLED` / `DO_NOT_TRACK` env vars suppress event delivery, not file writes (matches Go). - Hooks from each backups handler via Effect.ensuring(flush) — fires whether the main API call succeeds or fails (matches Go's PersistentPostRun). Updates integration test setups to provide a no-op mockTelemetryStateLayer. Completes the Go-parity infrastructure backups list/restore needed for cli-e2e parity tests to pass: filesystem snapshots now match Go's output byte-for-byte after normalize().
jgoux
approved these changes
May 21, 2026
Contributor
jgoux
left a comment
There was a problem hiding this comment.
Just a remark about e2e tests, solid work! 👍
Don't hesitate to add more instructions in AGENTS.md if you think they are helping the agent nailing the port (and we can all benefit from it later if we help!)
…mokes Captures the recurring Go-parity gotchas, hoisting policy, and helper-file shape that emerged from the backups port so the next agent doesn't relearn them from failing cli-e2e diffs. Also removes the two `backups list` / `backups restore` e2e files that were pure `--help` smoke tests — the exact pattern the workspace e2e policy forbids, now cited as the canonical "do not write" example.
f8df987 to
e68baaf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the Phase-0 Go-proxy handlers for
supabase backups listandsupabase backups restorewith native Effect-based implementations. Adds the supporting legacy infrastructure (legacy/auth,legacy/config, project-ref resolver, Glamour table renderer) that subsequent ports will reuse.Highlights
--output json(alphabetical struct-field order,backups: nullfor empty slices to match Go's nil-slice semantics), Glamour-styled tables verified byte-for-byte against Go test fixtures, restore stderr line preserved.Output.raw(text, stream)service method. Handlers now route stdout/stderr writes through theOutputservice instead of callingprocess.stdout/stderr.writedirectly.mockOutputcaptures these intorawChunks+stdoutText/stderrTextgetters, eliminating ~30 lines ofprocess.*.writemonkey-patching per integration test file.backups.layers.tsexposes alegacyBackupsRuntimeLayer(subcommand)factory so each subcommand wires the platform-API + project-ref stack identically. NewmapLegacyBackupHttpErrorfactory inbackups.errors.tsconsolidatesRESPONSE_ERROR_TAGS+ HTTP-error dispatch and truncates response bodies to 1024 chars before embedding them in tagged errors.*.command.tsfiles markconfig as constandexport type LegacyBackups*Flags = CliCommand.Command.Config.Infer<typeof config>(canonicallogin.command.tspattern); handlers import the type instead of duplicating private interfaces.output.task("Fetching backups...")/"Initiating PITR restore..."only run whenoutput.format === "text", eliminating dangling[task] start:lines on stderr in JSON / stream-json modes.packages/api/src/generated/contracts.tsrebuilt from upstream OpenAPI — adds the missingidfield on backup items, plus broader spec drift since the last sync.backups.encoders.unit.test.tsand the byte-stable--output jsonassertion (against the Go fixture fromapps/cli-go/internal/backups/list/list_test.go). Targeted e2e--helpsmoke tests for bothlistandrestore.Known Gaps (documented, not blocking)
V1RestorePitrBackupInput.recovery_time_target_unixretains an upstream>= 0constraint that Go'sint64does not enforce. A negative timestamp surfaces a local schema-decode error rather than the API's own error. Noted inrestore/SIDE_EFFECTS.md; resolving requires an upstream OpenAPI change.Reviewer Notes
effect-client.tsandopenapi.json. Diff scope is large but mechanical — all the meaningful schema deltas land incontracts.ts. One unrelatednext/snapshot expectation (platform-schema.integration.test.ts) updated to match the new upstream description text forv1ListAllProjects.Closes CLI-1301